SPB Git

spb/spboucher.ai Public

spboucher.ai — personal website of Simon-Pierre Boucher.

TypeScript 93.4% HTML 5.5% CSS 1%
6.6 KB · 199 lines tsx
Raw Blame History
1/*2  page.tsx3  spboucher.ai Web4  Author: Simon-Pierre Boucher5  Mail: contact@spboucher.ai6*/78import type { Metadata } from "next";9import Image from "next/image";10import Link from "next/link";11import { notFound } from "next/navigation";12import {13  ArrowLeft,14  ArrowRight,15  CalendarDays,16  Clock,17  Mail,18} from "lucide-react";1920import { Badge } from "@/components/ui/badge";21import { BlogContent } from "@/components/blog-content";22import { Reveal } from "@/components/reveal";23import { getBlogPost, getBlogPosts, getBlogSlugs } from "@/lib/blog";2425interface PageProps {26  params: Promise<{ slug: string }>;27}2829export function generateStaticParams() {30  return getBlogSlugs().map((slug) => ({ slug }));31}3233export async function generateMetadata({34  params,35}: PageProps): Promise<Metadata> {36  const { slug } = await params;37  const post = getBlogPost(slug);38  if (!post) return {};39  return {40    title: post.title,41    description: post.excerpt,42    openGraph: {43      title: post.title,44      description: post.excerpt,45      type: "article",46      publishedTime: post.date,47      authors: ["Simon-Pierre Boucher"],48    },49  };50}5152function AuthorSignature({ compact = false }: { compact?: boolean }) {53  return (54    <div className="flex items-center gap-4">55      <Image56        src="/profile/simon-pierre-boucher.jpeg"57        alt="Portrait of Simon-Pierre Boucher"58        width={96}59        height={96}60        className={`${compact ? "h-11 w-11" : "h-14 w-14"} rounded-full object-cover object-top ring-2 ring-primary/20`}61      />62      <div className="min-w-0">63        <p className="font-display text-sm font-semibold tracking-tight">64          Simon-Pierre Boucher65        </p>66        {!compact && (67          <p className="text-xs text-muted-foreground">68            Professor — Department of Administrative Sciences, UQO69          </p>70        )}71        <a72          href="mailto:contact@spboucher.ai"73          className="mt-0.5 inline-flex items-center gap-1.5 text-xs text-muted-foreground transition-colors hover:text-primary"74        >75          <Mail className="h-3 w-3" aria-hidden="true" />76          contact@spboucher.ai77        </a>78      </div>79    </div>80  );81}8283export default async function BlogPostPage({ params }: PageProps) {84  const { slug } = await params;85  const post = getBlogPost(slug);86  if (!post) notFound();8788  const posts = getBlogPosts();89  const index = posts.findIndex((p) => p.slug === slug);90  const previous = index > 0 ? posts[index - 1] : undefined;91  const next = index < posts.length - 1 ? posts[index + 1] : undefined;9293  return (94    <article className="mx-auto max-w-3xl px-4 py-16 sm:px-6">95      <Reveal>96        <Link97          href="/blog"98          className="inline-flex items-center gap-1.5 text-sm font-medium text-muted-foreground transition-colors hover:text-primary"99        >100          <ArrowLeft className="h-4 w-4" aria-hidden="true" />101          All essays102        </Link>103      </Reveal>104105      {/* Header */}106      <Reveal className="mt-8">107        <header>108          <div className="flex flex-wrap items-center gap-x-4 gap-y-2 text-xs font-medium text-muted-foreground">109            <Badge>Essay</Badge>110            <span className="inline-flex items-center gap-1.5">111              <CalendarDays112                className="h-3.5 w-3.5 text-primary/70"113                aria-hidden="true"114              />115              {post.dateLabel}116            </span>117            <span className="inline-flex items-center gap-1.5">118              <Clock className="h-3.5 w-3.5 text-primary/70" aria-hidden="true" />119              {post.readingMinutes} min read120            </span>121            <span className="flex flex-wrap gap-1.5">122              {post.tags.map((tag) => (123                <Badge key={tag} variant="outline">124                  {tag}125                </Badge>126              ))}127            </span>128          </div>129          <h1 className="mt-5 font-display text-3xl font-bold leading-tight tracking-tight sm:text-[2.75rem] sm:leading-[1.15]">130            {post.title}131          </h1>132          <div className="mt-7 border-y border-border/60 py-4">133            <AuthorSignature compact />134          </div>135        </header>136      </Reveal>137138      {/* Body */}139      <Reveal delay={0.08} className="mt-10">140        <BlogContent blocks={post.blocks} />141      </Reveal>142143      {/* Signature */}144      <Reveal className="mt-14">145        <footer className="border-y-2 border-foreground bg-card/50 p-6 sm:p-8">146          <p className="flex items-center gap-3 font-mono text-[0.68rem] font-medium uppercase tracking-[0.22em] text-primary">147            <span aria-hidden="true" className="h-px w-6 bg-primary/60" />148            Written by149          </p>150          <div className="mt-4">151            <AuthorSignature />152          </div>153        </footer>154      </Reveal>155156      {/* Prev / next */}157      {(previous || next) && (158        <Reveal className="mt-8">159          <nav160            aria-label="More essays"161            className="grid gap-3 sm:grid-cols-2"162          >163            {previous ? (164              <Link165                href={`/blog/${previous.slug}`}166                className="group rounded-3xl border border-border/60 bg-card/70 p-5 shadow-[var(--shadow-soft)] transition-all duration-300 hover:-translate-y-0.5 hover:border-primary/25"167              >168                <span className="inline-flex items-center gap-1 text-xs font-medium text-muted-foreground">169                  <ArrowLeft className="h-3.5 w-3.5" aria-hidden="true" />170                  Previous essay171                </span>172                <p className="mt-2 font-display text-sm font-semibold leading-snug tracking-tight transition-colors group-hover:text-primary">173                  {previous.title}174                </p>175              </Link>176            ) : (177              <span aria-hidden="true" />178            )}179            {next && (180              <Link181                href={`/blog/${next.slug}`}182                className="group rounded-3xl border border-border/60 bg-card/70 p-5 text-right shadow-[var(--shadow-soft)] transition-all duration-300 hover:-translate-y-0.5 hover:border-primary/25"183              >184                <span className="inline-flex items-center gap-1 text-xs font-medium text-muted-foreground">185                  Next essay186                  <ArrowRight className="h-3.5 w-3.5" aria-hidden="true" />187                </span>188                <p className="mt-2 font-display text-sm font-semibold leading-snug tracking-tight transition-colors group-hover:text-primary">189                  {next.title}190                </p>191              </Link>192            )}193          </nav>194        </Reveal>195      )}196    </article>197  );198}199